# coding: utf-8
"""
Pocky 博客图片格式加工器
依赖：Pillow
"""

import io, os, re, time, threading, urllib.request, urllib.parse, tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageTk

APP_TITLE = "Pocky 博客图片格式加工器"
APP_WIDTH, APP_HEIGHT = 980, 720
THUMB_MAX_W = 200  # 缩略图最大宽度（像素）

PLAIN_RE = re.compile(r"(!\[image-\d+]\()(.+?)(\))")
ZOOM_RE = re.compile(r'(<img src=")(.*)(" .* )style="zoom:\s?(\d+)%;"(\s?/>)')
ZOOM_EMPTY_RE = re.compile(r'(<img src=")(.*)(" .* )style="zoom:();"(\s?/>)')

# ---------- I/O & 图片读取 ----------
def read_text_any(path):
    for enc in ("utf-8", "utf-8-sig", "gbk", "cp936"):
        try:
            with open(path, "r", encoding=enc) as f:
                return f.read()
        except UnicodeDecodeError:
            continue
    with open(path, "rb") as f:
        return f.read().decode("utf-8", "ignore")

def _open_image_from_local(local_path):
    with open(local_path, "rb") as f:
        temp = io.BytesIO(f.read())
    img = Image.open(temp); img.load()
    return img

def _open_image_from_http(url):
    with urllib.request.urlopen(url, timeout=10) as resp:
        temp = io.BytesIO(resp.read())
    img = Image.open(temp); img.load()
    return img

def load_image_any(path_or_url, base_dir=None):
    """返回 PIL.Image 或 None。支持 http(s)、file://、相对/绝对路径。"""
    try:
        u = path_or_url.strip()
        if u.lower().startswith("file://"):
            parsed = urllib.parse.urlparse(u)
            local_path = urllib.request.url2pathname(parsed.path)
            if os.name == "nt" and local_path.startswith("/") and len(local_path) > 2 and local_path[2] == ":":
                local_path = local_path[1:]
            return _open_image_from_local(local_path)
        if u.lower().startswith(("http://", "https://")):
            return _open_image_from_http(u)
        local_path = u
        if base_dir and not os.path.isabs(local_path):
            local_path = os.path.normpath(os.path.join(base_dir, local_path))
        if os.path.exists(local_path):
            return _open_image_from_local(local_path)
        return None
    except Exception:
        return None

def get_size_from_image(img):
    try:
        return img.size
    except Exception:
        return (0, 0)

def count_total(content):
    return (len(list(PLAIN_RE.finditer(content))) +
            len(list(ZOOM_RE.finditer(content))) +
            len(list(ZOOM_EMPTY_RE.finditer(content))))

def fmt_eta(sec):
    m, s = divmod(int(sec), 60); h, m = divmod(m, 60)
    return f"{h:02d}:{m:02d}:{s:02d}" if h else f"{m:02d}:{s:02d}"

# ---------- 处理器 ----------
class Processor:
    def __init__(self, content, base_dir, progress_cb=None, total=None):
        self.content = content
        self.base_dir = base_dir
        self.progress_cb = progress_cb
        self.total = total or 0
        self.done = 0
        self.success = 0
        self.start_ts = time.time()
        self.logs = []      # [ (idx, label_text, pil_image or None) ]
        self.out_md = ""

    def _tick(self, ok):
        self.done += 1
        if ok: self.success += 1
        elapsed = time.time() - self.start_ts
        avg = elapsed / self.done if self.done else 0.0
        remain = max(self.total - self.done, 0)
        eta_sec = int(avg * remain)
        if self.progress_cb: self.progress_cb(self.done, self.total, eta_sec)

    def _fmt_label(self, w, h):
        return f"#pic_center ={w}x{h}"  # 严格格式

    def _resize_thumb(self, img):
        w, h = img.size
        if w <= THUMB_MAX_W: return img
        ratio = THUMB_MAX_W / float(w)
        return img.resize((THUMB_MAX_W, max(1, int(h * ratio))), Image.LANCZOS)

    def _repl_plain(self, m):
        url = m.group(2)
        img = load_image_any(url, self.base_dir)
        w, h = get_size_from_image(img) if img else (0, 0)
        label = self._fmt_label(w, h)
        thumb = self._resize_thumb(img) if img else None
        self.logs.append((len(self.logs)+1, label, thumb))
        self._tick(bool(img))
        return "![](" + url + "#pic_center)"

    def _repl_zoom(self, m):
        url = m.group(2)
        try: zoom_value = int(m.group(4)) / 100.0
        except Exception: zoom_value = 1.0
        img = load_image_any(url, self.base_dir)
        if img:
            ow, oh = img.size
            w, h = int(zoom_value * ow), int(zoom_value * oh)
        else:
            w, h = (0, 0)
        label = self._fmt_label(w, h)
        thumb = self._resize_thumb(img) if img else None
        self.logs.append((len(self.logs)+1, label, thumb))
        self._tick(bool(img))
        return "![](" + url + "#pic_center" + f" ={w}x{h})"

    def _repl_zoom_empty(self, m):
        url = m.group(2)
        img = load_image_any(url, self.base_dir)
        w, h = get_size_from_image(img) if img else (0, 0)
        label = self._fmt_label(w, h)
        thumb = self._resize_thumb(img) if img else None
        self.logs.append((len(self.logs)+1, label, thumb))
        self._tick(bool(img))
        return "![](" + url + "#pic_center)"

    def process(self):
        out1 = PLAIN_RE.sub(self._repl_plain, self.content)
        out2 = ZOOM_RE.sub(self._repl_zoom, out1)
        out3 = ZOOM_EMPTY_RE.sub(self._repl_zoom_empty, out2)
        self.out_md = out3
        return self.out_md, self.logs, self.success

# ---------- 可滚动缩略图容器 ----------
class ScrollThumbs(ttk.Frame):
    def __init__(self, master, **kw):
        super().__init__(master, **kw)
        self.canvas = tk.Canvas(self, highlightthickness=0, bd=0, bg="#ffffff")
        self.scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
        self.inner = ttk.Frame(self.canvas, style="Thumbs.TFrame")
        self.inner_id = self.canvas.create_window((0,0), window=self.inner, anchor="nw")
        self.canvas.configure(yscrollcommand=self.scrollbar.set)
        self.canvas.pack(side="left", fill="both", expand=True)
        self.scrollbar.pack(side="right", fill="y")
        self.inner.bind("<Configure>", self._on_configure)
        self.canvas.bind("<Configure>", self._on_canvas_configure)

        # 鼠标滚轮（进入时绑定，离开时解绑；Win/macOS/Linux 兼容）
        self.canvas.bind("<Enter>", lambda e: self._bind_mousewheel(True))
        self.canvas.bind("<Leave>", lambda e: self._bind_mousewheel(False))

        # 防 GC
        self._photo_refs = []
        self._var_refs = []

    def _on_configure(self, event):
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

    def _on_canvas_configure(self, event):
        self.canvas.itemconfig(self.inner_id, width=event.width)

    def _bind_mousewheel(self, enable: bool):
        if enable:
            # 让 canvas 接收滚轮
            self.canvas.focus_set()
            # Windows / macOS
            self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
            # Linux
            self.canvas.bind_all("<Button-4>", self._on_mousewheel)
            self.canvas.bind_all("<Button-5>", self._on_mousewheel)
        else:
            self.canvas.unbind_all("<MouseWheel>")
            self.canvas.unbind_all("<Button-4>")
            self.canvas.unbind_all("<Button-5>")

    def _on_mousewheel(self, event):
        if event.num == 4:  # Linux 上滚上
            self.canvas.yview_scroll(-3, "units")
        elif event.num == 5:  # Linux 上滚下
            self.canvas.yview_scroll(3, "units")
        else:
            # Windows/macOS: delta 为 120 的倍数
            self.canvas.yview_scroll(int(-event.delta/120)*3, "units")

    def clear(self):
        for w in self.inner.winfo_children():
            w.destroy()
        self._photo_refs.clear()
        self._var_refs.clear()

    def _copy_to_clip(self, text):
        top = self.winfo_toplevel()
        top.clipboard_clear()
        top.clipboard_append(text)

    def add_item(self, index, label_text, pil_img):
        card = ttk.Frame(self.inner, padding=10, style="Card.TFrame")
        card.pack(fill="x", anchor="n", pady=(6, 6))

        row = ttk.Frame(card); row.pack(fill="x")
        idx_lbl = ttk.Label(row, text=f"{index:02d}", style="Muted.TLabel", width=4, anchor="e")
        idx_lbl.grid(row=0, column=0, rowspan=2, sticky="n", padx=(0, 8))

        if pil_img:
            photo = ImageTk.PhotoImage(pil_img)
            self._photo_refs.append(photo)
            img_lbl = ttk.Label(row, image=photo)
        else:
            img_lbl = ttk.Label(row, text="[加载失败]", width=24, style="Muted.TLabel")
        img_lbl.grid(row=0, column=1, rowspan=2, sticky="w", padx=(0, 12))

        attr_frame = ttk.Frame(row)
        attr_frame.grid(row=0, column=2, sticky="we")
        row.columnconfigure(2, weight=1)

        var = tk.StringVar(value=label_text)
        self._var_refs.append(var)
        attr_ent = tk.Entry(
            attr_frame, textvariable=var, state="readonly",
            relief="flat", readonlybackground="#ffffff",
            highlightthickness=1, highlightcolor="#c9d4df", highlightbackground="#c9d4df",
            fg="#111827", insertbackground="#111827", borderwidth=6
        )
        attr_ent.pack(fill="x", expand=True)
        attr_ent.bind("<Button-1>", lambda e, w=attr_ent: w.focus_set())
        attr_ent.bind("<Control-a>", lambda e, w=attr_ent: (w.selection_range(0, "end"), "break"))

        btn = ttk.Button(attr_frame, text="复制该属性", style="Accent.TButton",
                         command=lambda s=label_text: self._copy_to_clip(s))
        btn.pack(anchor="w", pady=(8, 0))

# ---------- 应用 UI ----------
class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title(APP_TITLE)
        self.geometry(f"{APP_WIDTH}x{APP_HEIGHT}")
        self.minsize(940, 640)
        self.configure(bg="#f5f7fa")
        self._setup_light_theme()

        self.var_infile = tk.StringVar(value="（尚未选择文件）")
        self.var_outdir = tk.StringVar(value="（尚未选择输出文件夹）")
        self.status_text = tk.StringVar(value="请选择 Markdown 文件与输出文件夹。")
        self.eta_text = tk.StringVar(value="ETA 00:00")
        self.success_count = 0
        self.total_count = 0
        self.processed_md = ""

        self._build_ui()

    def _setup_light_theme(self):
        style = ttk.Style(self)
        # 统一使用 clam，便于自定义前景/背景色
        style.theme_use("clam")

        # 基础色
        bg = "#f5f7fa"      # 背景
        card = "#ffffff"    # 卡片
        fg = "#1f2328"      # 文字
        muted = "#6b7280"   # 次级文字
        border = "#e5e7eb"  # 边框
        accent = "#4c8bf5"  # 强调色（按钮/进度）

        # 全局
        style.configure(".", background=bg, foreground=fg, font=("Segoe UI", 10))
        # 标签
        style.configure("TLabel", background=bg, foreground=fg)
        style.configure("Muted.TLabel", background=bg, foreground=muted)
        # 普通按钮（黑字）
        style.configure("TButton", padding=8, background="#e9eef6", foreground=fg, relief="flat")
        style.map("TButton",
                  background=[("active", "#dde7f3"), ("pressed", "#d0dcec")],
                  foreground=[("disabled", "#b0b8c4")])
        # 强调按钮（蓝底白字）
        style.configure("Accent.TButton", padding=8, background=accent, foreground="#ffffff", relief="flat")
        style.map("Accent.TButton",
                  background=[("active", "#3b79ef"), ("pressed", "#316be0"), ("disabled", "#a7c2ff")],
                  foreground=[("disabled", "#e9eef6")])
        # 进度条
        style.configure("Horizontal.TProgressbar", troughcolor=border, background=accent, thickness=10)
        # 卡片容器（缩略图列表每项）
        style.configure("Card.TFrame", background=card, relief="groove", borderwidth=1)
        style.configure("Thumbs.TFrame", background=bg)

    def _build_ui(self):
        pad = 12
        top = ttk.Frame(self, padding=pad, style="Card.TFrame"); top.pack(fill="x", padx=12, pady=(12, 8))
        row1 = ttk.Frame(top); row1.pack(fill="x", pady=(0,6))
        ttk.Label(row1, text="输入 Markdown：", width=16).pack(side="left")
        ttk.Entry(row1, textvariable=self.var_infile).pack(side="left", expand=True, fill="x", padx=(6,6))
        ttk.Button(row1, text="浏览…", command=self.choose_file).pack(side="left")

        row2 = ttk.Frame(top); row2.pack(fill="x")
        ttk.Label(row2, text="输出文件夹：", width=16).pack(side="left")
        ttk.Entry(row2, textvariable=self.var_outdir).pack(side="left", expand=True, fill="x", padx=(6,6))
        ttk.Button(row2, text="选择文件夹…", command=self.choose_outdir).pack(side="left")

        mid = ttk.Frame(self, padding=(12, 0, 12, 0)); mid.pack(fill="both", expand=True)
        toolbar = ttk.Frame(mid); toolbar.pack(fill="x", pady=(0,6))
        self.btn_process = ttk.Button(toolbar, text="开始处理", style="Accent.TButton",
                                      command=self.start_process, state="disabled")
        self.btn_process.pack(side="left")

        row_prog = ttk.Frame(mid); row_prog.pack(fill="x")
        self.progress = ttk.Progressbar(row_prog, mode="determinate", maximum=100)
        self.progress.pack(side="left", fill="x", expand=True, pady=(0, 8))
        ttk.Label(row_prog, textvariable=self.eta_text, style="Muted.TLabel").pack(side="left", padx=(10,0))

        nb = ttk.Notebook(mid); nb.pack(fill="both", expand=True, pady=(0, 10))
        self.thumbs = ScrollThumbs(nb); nb.add(self.thumbs, text="清单（缩略图）")
        self.txt_md = tk.Text(nb, wrap="none", height=18, undo=False,
                              bg="#ffffff", fg="#111827", insertbackground="#111827")
        nb.add(self.txt_md, text="处理后 Markdown 预览")

        bottom = ttk.Frame(self, padding=(12,0,12,12))
        bottom.pack(fill="x")
        ttk.Label(bottom, textvariable=self.status_text, style="Muted.TLabel").pack(side="left")

    # 选择输入文件
    def choose_file(self):
        path = filedialog.askopenfilename(
            title="选择 Markdown 文件",
            filetypes=[("Markdown 文件", "*.md *.markdown"), ("所有文件", "*.*")]
        )
        if not path: return
        self.var_infile.set(path)
        self._update_start_state()

    # 选择输出文件夹（必选）
    def choose_outdir(self):
        d = filedialog.askdirectory(title="选择输出文件夹")
        if not d: return
        self.var_outdir.set(d)
        self._update_start_state()

    def _update_start_state(self):
        infile = self.var_infile.get()
        outdir = self.var_outdir.get()
        ready = os.path.isfile(infile) and os.path.isdir(outdir)
        self.btn_process.config(state=("normal" if ready else "disabled"))
        self.status_text.set("就绪：点击“开始处理”。" if ready else "请选择 Markdown 文件与输出文件夹。")

    # 开始处理
    def start_process(self):
        infile = self.var_infile.get().strip()
        outdir = self.var_outdir.get().strip()
        if not os.path.isfile(infile):
            messagebox.showerror("错误", "请选择有效的 Markdown 文件。"); return
        if not os.path.isdir(outdir):
            messagebox.showerror("错误", "请选择有效的输出文件夹。"); return

        self.btn_process.config(state="disabled")
        self.progress.configure(maximum=100, value=0)
        self.eta_text.set("ETA 00:00")
        self.status_text.set("处理中…")
        self.thumbs.clear()
        self.txt_md.delete("1.0", "end")

        def worker():
            try:
                base_dir = os.path.dirname(infile)
                content = read_text_any(infile)
                total = count_total(content); self.total_count = total

                def on_progress(done, total_, eta_sec):
                    pct = 0 if total_ == 0 else int(done * 100 / total_)
                    self.after(0, lambda: self._update_progress(pct, eta_sec))

                proc = Processor(content, base_dir, progress_cb=on_progress, total=total)
                md_result, logs, success = proc.process()
                self.processed_md = md_result; self.success_count = success

                out_path = os.path.join(outdir, "CSDN_" + os.path.basename(infile))
                with open(out_path, "w", encoding="utf-8") as f:
                    f.write(md_result)

                def fill_ui():
                    self.txt_md.insert("1.0", md_result)
                    for idx, label, pil_img in logs:
                        self.thumbs.add_item(idx, label, pil_img)
                    self._finish_status(total, success, out_path)
                self.after(0, fill_ui)

            except Exception as e:
                self.after(0, lambda: self._error(str(e)))

        threading.Thread(target=worker, daemon=True).start()

    def _update_progress(self, percent, eta_sec):
        self.progress.configure(value=percent)
        self.eta_text.set(f"ETA {fmt_eta(eta_sec)}")

    def _finish_status(self, total, ok, out_path):
        self.status_text.set(f"完成：共检测到{total}张图片，加工成功{ok}张图片  |  已保存：{out_path}")
        self.btn_process.config(state="normal")
        self.progress.configure(value=100)
        self.eta_text.set("ETA 00:00")

    def _error(self, msg):
        self.status_text.set(f"处理失败：{msg}")
        self.btn_process.config(state="normal")
        self.progress.configure(value=0)
        self.eta_text.set("ETA 00:00")

def main():
    app = App()
    app.mainloop()

if __name__ == "__main__":
    main()
